#!/usr/bin/python

# Copyright (c) 2016-2023, Vlad Glagolev <scm@vaygr.net>
#
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import annotations

DOCUMENTATION = r"""
module: simpleinit_msb
short_description: Manage services on Source Mage GNU/Linux
version_added: 7.5.0
description:
  - Controls services on remote hosts using C(simpleinit-msb).
author: "Vlad Glagolev (@vaygr)"
extends_documentation_fragment:
  - community.general.attributes
attributes:
  check_mode:
    support: full
  diff_mode:
    support: none
options:
  name:
    type: str
    description:
      - Name of the service.
    required: true
    aliases: ['service']
  state:
    type: str
    choices: [running, started, stopped, restarted, reloaded]
    description:
      - V(started)/V(stopped) are idempotent actions that do not run commands unless necessary. V(restarted) always bounces
        the service. V(reloaded) always reloads.
      - At least one of O(state) and O(enabled) are required.
      - Note that V(reloaded) starts the service if it is not already started, even if your chosen init system would not normally.
  enabled:
    type: bool
    description:
      - Whether the service should start on boot.
      - At least one of O(state) and O(enabled) are required.
"""

EXAMPLES = r"""
- name: Example action to start service httpd, if not running
  community.general.simpleinit_msb:
    name: httpd
    state: started

- name: Example action to stop service httpd, if running
  community.general.simpleinit_msb:
    name: httpd
    state: stopped

- name: Example action to restart service httpd, in all cases
  community.general.simpleinit_msb:
    name: httpd
    state: restarted

- name: Example action to reload service httpd, in all cases
  community.general.simpleinit_msb:
    name: httpd
    state: reloaded

- name: Example action to enable service httpd, and not touch the running state
  community.general.simpleinit_msb:
    name: httpd
    enabled: true
"""

import os
import re

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.service import daemonize


class SimpleinitMSB:
    """
    Main simpleinit-msb service manipulation class
    """

    def __init__(self, module):
        self.module = module
        self.name = module.params["name"]
        self.state = module.params["state"]
        self.enable = module.params["enabled"]
        self.changed = False
        self.running = None
        self.action = None
        self.telinit_cmd = None
        self.svc_change = False

    def execute_command(self, cmd, daemon=False):
        if not daemon:
            return self.module.run_command(cmd)
        else:
            return daemonize(self.module, cmd)

    def check_service_changed(self):
        if self.state and self.running is None:
            self.module.fail_json(msg="failed determining service state, possible typo of service name?")
        # Find out if state has changed
        if not self.running and self.state in ["started", "running", "reloaded"]:
            self.svc_change = True
        elif self.running and self.state in ["stopped", "reloaded"]:
            self.svc_change = True
        elif self.state == "restarted":
            self.svc_change = True
        if self.module.check_mode and self.svc_change:
            self.module.exit_json(changed=True, msg="service state changed")

    def modify_service_state(self):
        # Only do something if state will change
        if self.svc_change:
            # Control service
            if self.state in ["started", "running"]:
                self.action = "start"
            elif not self.running and self.state == "reloaded":
                self.action = "start"
            elif self.state == "stopped":
                self.action = "stop"
            elif self.state == "reloaded":
                self.action = "reload"
            elif self.state == "restarted":
                self.action = "restart"

            if self.module.check_mode:
                self.module.exit_json(changed=True, msg="changing service state")

            return self.service_control()
        else:
            # If nothing needs to change just say all is well
            rc = 0
            err = ""
            out = ""
            return rc, out, err

    def get_service_tools(self):
        paths = ["/sbin", "/usr/sbin", "/bin", "/usr/bin"]
        binaries = ["telinit"]
        location = dict()

        for binary in binaries:
            location[binary] = self.module.get_bin_path(binary, opt_dirs=paths)

        if location.get("telinit", False) and os.path.exists("/etc/init.d/smgl_init"):
            self.telinit_cmd = location["telinit"]

        if self.telinit_cmd is None:
            self.module.fail_json(msg="cannot find telinit script for simpleinit-msb, aborting...")

    def get_service_status(self):
        self.action = "status"
        rc, status_stdout, status_stderr = self.service_control()

        if self.running is None and status_stdout.count("\n") <= 1:
            cleanout = status_stdout.lower().replace(self.name.lower(), "")

            if "is not running" in cleanout:
                self.running = False
            elif "is running" in cleanout:
                self.running = True

        return self.running

    def service_enable(self):
        # Check if the service is already enabled/disabled
        if not self.enable ^ self.service_enabled():
            return

        action = f"boot{'enable' if self.enable else 'disable'}"

        (rc, out, err) = self.execute_command(f"{self.telinit_cmd} {action} {self.name}")

        self.changed = True

        for line in err.splitlines():
            if self.enable and line.find("already enabled") != -1:
                self.changed = False
                break
            if not self.enable and line.find("already disabled") != -1:
                self.changed = False
                break

        if not self.changed:
            return

        return (rc, out, err)

    def service_enabled(self):
        self.service_exists()

        (rc, out, err) = self.execute_command(f"{self.telinit_cmd} {self.enable}d")

        service_enabled = not self.enable

        rex = re.compile(rf"^{self.name}$")

        for line in out.splitlines():
            if rex.match(line):
                service_enabled = True if self.enable else False
                break

        return service_enabled

    def service_exists(self):
        (rc, out, err) = self.execute_command(f"{self.telinit_cmd} list")

        service_exists = False

        rex = re.compile(rf"^\w+\s+{self.name}$")

        for line in out.splitlines():
            if rex.match(line):
                service_exists = True
                break

        if not service_exists:
            self.module.fail_json(msg=f"telinit could not find the requested service: {self.name}")

    def service_control(self):
        self.service_exists()

        svc_cmd = f"{self.telinit_cmd} run {self.name}"

        rc_state, stdout, stderr = self.execute_command(f"{svc_cmd} {self.action}", daemon=True)

        return (rc_state, stdout, stderr)


def build_module():
    return AnsibleModule(
        argument_spec=dict(
            name=dict(required=True, aliases=["service"]),
            state=dict(choices=["running", "started", "stopped", "restarted", "reloaded"]),
            enabled=dict(type="bool"),
        ),
        supports_check_mode=True,
        required_one_of=[["state", "enabled"]],
    )


def main():
    module = build_module()

    service = SimpleinitMSB(module)

    rc = 0
    out = ""
    err = ""
    result = {}
    result["name"] = service.name

    # Find service management tools
    service.get_service_tools()

    # Enable/disable service startup at boot if requested
    if service.module.params["enabled"] is not None:
        service.service_enable()
        result["enabled"] = service.enable

    if module.params["state"] is None:
        # Not changing the running state, so bail out now.
        result["changed"] = service.changed
        module.exit_json(**result)

    result["state"] = service.state

    service.get_service_status()

    # Calculate if request will change service state
    service.check_service_changed()

    # Modify service state if necessary
    (rc, out, err) = service.modify_service_state()

    if rc != 0:
        if err:
            module.fail_json(msg=err)
        else:
            module.fail_json(msg=out)

    result["changed"] = service.changed | service.svc_change
    if service.module.params["enabled"] is not None:
        result["enabled"] = service.module.params["enabled"]

    if not service.module.params["state"]:
        status = service.get_service_status()
        if status is None:
            result["state"] = "absent"
        elif status is False:
            result["state"] = "started"
        else:
            result["state"] = "stopped"
    else:
        # as we may have just bounced the service the service command may not
        # report accurate state at this moment so just show what we ran
        if service.module.params["state"] in ["started", "restarted", "running", "reloaded"]:
            result["state"] = "started"
        else:
            result["state"] = "stopped"

    module.exit_json(**result)


if __name__ == "__main__":
    main()
